// --------------------------------------------------------------------------------------------------------------------
// <copyright file="MockNetworkStream.cs" company="Microsoft Corporation">
// The MIT License (MIT)
// 
// Copyright (c) 2014, Microsoft Corporation
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace Test.Robotics.Hardware.UniversalRobots
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using System.Threading.Tasks;

    /// <summary>
    /// A mock network stream, designed specifically for testing URControlClient
    /// </summary>
    public class MockNetworkStream : MemoryStream
    {
        /// <summary>
        /// Packet to send when stream is read.
        /// </summary>
        private byte[] sendPacket;

        /// <summary>
        /// Last packet written to stream.
        /// </summary>
        private byte[] lastReceivedPacket;

        /// <summary>
        /// Delay between calling ReadAsync and a success result.
        /// </summary>
        private int delayBetweenPacketsInMs;

        /// <summary>
        /// Number of packets written to stream.
        /// </summary>
        private int receivedPacketCount;

        /// <summary>
        /// An object for locking on
        /// </summary>
        private object lockObject;

        /// <summary>
        /// Has at least one packet been read.
        /// </summary>
        private bool singlePacketRead = false;

        /// <summary>
        /// Initializes a new instance of the <see cref="MockNetworkStream"/> class.
        /// </summary>
        /// <param name="packetToSend">Packet to be sent in response to ReadAsync requests.</param>
        /// <param name="delayBetweenPacketsInMs">Delay between calling ReadAsync and a success result being returned.</param>
        public MockNetworkStream(byte[] packetToSend, int delayBetweenPacketsInMs)
        {
            this.sendPacket = new byte[packetToSend.Length];
            Array.Copy(packetToSend, this.sendPacket, packetToSend.Length);

            this.delayBetweenPacketsInMs = delayBetweenPacketsInMs;
            this.receivedPacketCount = 0;

            this.lockObject = new object();
        }

        /// <summary>
        /// Gets a value indicating if the stream can be read from.
        /// </summary>
        public override bool CanRead
        {
            get 
            { 
                return true; 
            }
        }

        /// <summary>
        /// Gets a value indicating if the stream can be written to.
        /// </summary>
        public override bool CanWrite
        {
            get 
            { 
                return true; 
            }
        }

        /// <summary>
        /// Gets or sets read timeout.
        /// </summary>
        public override int ReadTimeout { get; set; }

        /// <summary>
        /// Gets the number of packets that have been written to stream.
        /// </summary>
        public int ReceivedPacketCount
        {
            get
            {
                lock (this.lockObject)
                {
                    return this.receivedPacketCount;
                }
            }
        }

        /// <summary>
        /// Gets the last packet written to the stream.
        /// </summary>
        public byte[] LastReceivedPacket
        {
            get
            {
                lock (this.lockObject)
                {
                    return this.lastReceivedPacket;
                }
            }
        }

        /// <summary>
        /// Perform a synchronous read.
        /// </summary>
        /// <param name="buffer">Buffer to read data into.</param>
        /// <param name="offset">Offset into buffer to write to.</param>
        /// <param name="count">Maximum number of bytes to be read.</param>
        /// <returns>Number of bytes read.</returns>
        public override int Read(byte[] buffer, int offset, int count)
        {
            if (buffer.Length < count + offset)
            {
                throw new ArgumentException("Buffer is not large enough for given offset and count");
            }

            int bytesToWrite = Math.Min(count, this.sendPacket.Length);

            Array.Copy(this.sendPacket, 0, buffer, offset, bytesToWrite);

            this.singlePacketRead = true;

            return bytesToWrite;
        }

        /// <summary>
        /// Perform an asynchronous read.
        /// </summary>
        /// <param name="buffer">Buffer to read data into.</param>
        /// <param name="offset">Offset into buffer to write to.</param>
        /// <param name="count">Maximum number of bytes to be read.</param>
        /// <param name="cancellationToken">A cancellation token.</param>
        /// <returns>A task with result indicating number of bytes read.</returns>
        public override Task<int> ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken)
        {
            if (this.singlePacketRead && this.delayBetweenPacketsInMs < 0)
            {
                Task<int> nonConcludingTask = new Task<int>(
                    () =>
                    {
                        try
                        {
                            Task.Delay(-1, cancellationToken).Wait();
                        }
                        catch (AggregateException e)
                        {
                            // A single TaskCanceled exception is expected during shutdown
                            // Any other combinations of exceptions is un-handled here.
                            if (e.InnerExceptions.Count > 1 || !(e.InnerException is TaskCanceledException))
                            {
                                throw;
                            }
                        }

                        return 0;
                    },
                    cancellationToken);

                nonConcludingTask.Start();
                return nonConcludingTask;
            }

            int bytesToWrite = this.Read(buffer, offset, count);

            // make sure we don't pass a negative delay to Task.Delay() (should result in an exception).
            int packetDelay = Math.Max(this.delayBetweenPacketsInMs, 0);

            Task<int> readAsyncTask = new Task<int>(
                () =>
                {
                    Task.Delay(packetDelay, cancellationToken).Wait();
                    return bytesToWrite;
                },
                cancellationToken);

            try
            {
                readAsyncTask.Start();
            }
            catch (AggregateException)
            {
                if (!(cancellationToken.IsCancellationRequested))
                {
                    throw;
                }
            }

            return readAsyncTask;
        }

        /// <summary>
        /// Write a packet to stream.
        /// </summary>
        /// <param name="buffer">Buffer containing data to write.</param>
        /// <param name="offset">Offset to start reading data from <c>buffer</c> at.</param>
        /// <param name="count">Number of bytes to write.</param>
        public override void Write(byte[] buffer, int offset, int count)
        {
            lock (this.lockObject)
            {
                byte[] packet = new byte[count];
                Array.Copy(buffer, offset, packet, 0, count);
                this.lastReceivedPacket = packet;
                this.receivedPacketCount++;
            }
        }
    }
}
